package core;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.swing.text.html.HTMLDocument.Iterator;

import bytecode.Bytecode;
import bytecode.DebugBytecode;
import bytecode.Kinds;
import cvm.CVM;
import cvm.DebugInfo;
import cvm.FunctionInfo;
import cvm.MethodArea;
import cvm.Pool;
import cvm.StackArea;
import dataStructure.ANode;

public class Loader {
	
	/* with a loader this DebugInfo can be convert into executable bytecode 
	 * the task:
	 * 1. replace the local variable symbols by the relative address 
	 * 2. replace the global variable symbols by it Pool address
	 * 3. push the real literal and string literal to the Pool and replace them by their Pool address
	 * 4. make the Pool
	 * 5. load the bytecode into the MethodArea (one rule to obey)
	 * 5. with the information of function to generate FunctionInfo[]
	 * 6. initialize the Stack(one rule to obey)
	 * 7. replace the function name by its address
	 * 8. get ready to run
	 * 9. if debug, keeps what variables we have currently and their address
	 * 10. if debug, the keeps the breakpoint location in the source file and run in CVM DebugMode 
	 * */
	
	private Pool pool;
	private FunctionInfo[] finfo;
	private String[] strings;
	private double[] floats;
	private Object[] globalVariables;
	private String[] globalVariableNames;

	
	private static int pc;
	private static int whichLocalEnv;
	
	private Aer aer;	
	private DebugInfo debugInfo;
	
	
	private List<String[]> functionEnvs;
	private List<String> functionName;
	/**
	 * if hit this pc, just stop because the return statement it points to means nothing
	 */
	public static int end_pc_of_main;
	private static int end_line_of_main;
	public static int main_index;
	
	private static Map<Integer, Integer> breakPointsPC;
	private static int[] breakLines;
	private static int bpindex;
	
	MethodArea bytecodes;
	
	private void loadFunctionEnvs()
	{
		whichLocalEnv = 0;
		for(HashMap<String,ANode> e : aer.getLocalEnvs())
		{
			// 1. add all the parameter to the arrayList in order
			int locals = aer.getLocalVariableCount(whichLocalEnv);
			ArrayList<String> tmp = new ArrayList<String>();
			functionEnvs.add(new String[locals]);
			e.forEach((s,a)->{
				if(a.getAttribute("Parameter Index")!=null)
					functionEnvs.get(whichLocalEnv)[(int)a.getAttribute("Parameter Index")-1]=(String) a.getAttribute("Symbol");
			});
			// 2. add the local variable above the parameters
			e.forEach((s,a)->{
				if(a.getAttribute("Parameter Index")==null)
					{
		     			tmp.add((String)a.getAttribute("Symbol"));
					}
			});
			ANode d = aer.functions.get(whichLocalEnv);
			int params = (int)d.getAttribute("Parameter Size");
			for(int i=0; i< tmp.size(); i++)
			{
				functionEnvs.get(whichLocalEnv)[params+i] = tmp.get(i);
			}
			finfo[whichLocalEnv] = new FunctionInfo(locals,0,(int)d.getAttribute("End Line"),params);
			whichLocalEnv++;
		}
	}
	

	
	public Loader(Aer aer, ArrayList<Integer> bp) {
		
		if(bp!=null)
		{
			Collections.sort(bp);
			breakLines = new int[bp.size()];
			for(int i=0; i<bp.size(); i++)
			{
				breakLines[i] = bp.get(i);
			}
			bpindex = 0;
			breakPointsPC = new HashMap<Integer, Integer>();
		}
		
		this.aer=aer;
		this.debugInfo=aer.getOut();

		//functions
		this.functionEnvs = new ArrayList<String[]>();
		this.finfo = new FunctionInfo[aer.functionNames.size()];		
		this.loadFunctionEnvs();
		this.functionName = aer.functionNames;
		//for main specially
		for(int i1=0; i1<finfo.length; i1++)
		{
			if("main".equals(functionName.get(i1)))
			{
				end_line_of_main = finfo[i1].getLine();
				main_index = i1;
			}
		}

		//pool
		this.strings = aer.strings.toArray(new String[1]);
		this.floats = new double[aer.floats.size()];
		int i = 0;
		for(Object f : aer.floats)
		{
			this.floats[i++] = (double) f;
		}		
		this.globalVariables = aer.globalVars;
		this.globalVariableNames = aer.globalVarName;
		this.pool = new Pool(strings,finfo,floats,globalVariables);
		
		
		
		//output bytecode
		this.bytecodes = new MethodArea();
		
		this.convertDebugByteCode();
		
		System.out.println("After Loader conversion:");
		bytecodes.printMethodArea();

	}

	/**
	 * convert DebugByteCode to ByteCode
	 */
	public void convertDebugByteCode() {
		int currentLine=0;

		whichLocalEnv=0;
		pc=1;
		finfo[0].setPc(pc);

		//we can add the breakPointLines test here, but it is fine for not checking
		//if we need to, skip the negative and use aer.getLastLine for the last line
		
		ArrayList<DebugBytecode> debugcodes = null;
		
		for(currentLine=0;currentLine<aer.getLastLine();currentLine++){
			
			if(breakLines!=null&&bpindex<breakLines.length&&currentLine==breakLines[bpindex])
			{
				breakPointsPC.put(pc+1, whichLocalEnv);
				bpindex++;
			}
			
			if((debugcodes=debugInfo.debugCodes.get(currentLine))==null)
				continue;
			for(DebugBytecode debugCode:debugcodes) {
				switch(debugCode.getKind()) {
				case vload:
					debugCode.setAddress(getSymbolAddress(debugCode.getSymbol() ,debugCode.getByteCode().getOp()));
					break;
				case vstore:
					debugCode.setAddress(getSymbolAddress(debugCode.getSymbol(),debugCode.getByteCode().getOp()));
					break;
				case fpush:
					debugCode.setAddress(fpush(debugCode.getSymbol()));
					break;
				case debugBytecodePushString:
					debugBytecodePushString(debugCode);
					break;
				case debugBytecodeGetAddress:
					debugBytecodeGetAddress(debugCode);
					break;
				case invoke:
					debugCode.setAddress(invoke(debugCode.getSymbol()));
					break;
				default:
					break;
				}
				pc++;
				bytecodes.add(debugCode.getBytecode());
			  }
			if(currentLine == end_line_of_main)
			{
				end_pc_of_main = pc-1;
				bytecodes.setMainEnd(end_pc_of_main);
			}
			if(currentLine==finfo[whichLocalEnv].getLine()) {
				whichLocalEnv++;
				if(whichLocalEnv<functionName.size())
				    finfo[whichLocalEnv].setPc(pc);
			}
			}

		}
		
	
	public int getSymbolAddress (String symbol ,int offset) {
		
		int op=0;
		if((op=this.getLocalVarIndex(symbol))!=0) {
			op=getLocalVarIndex(symbol)+offset;
		}else{
			op= 0-(getGlobalVarIndex(symbol))-offset;
		}
		return op;
	}
	
	public int invoke(String symbol) {
		int op=getFunctionIndex(symbol);
		return op;	
	}
	
	public int fpush(String symbol) {
		int op=getFloatIndex(symbol);
		return op;
	}
	
	public void debugBytecodePushString(DebugBytecode dbc) {
		dbc.setBytecode(new Bytecode(Kinds.push,getStringIndex(dbc.getSymbol())));	
	}
	
	public int debugBytecodeGetAddress(DebugBytecode dbc) {
		int op = this.getSymbolAddress(dbc.getSymbol(), 0);
		dbc.setBytecode(new Bytecode(Kinds.apush,op));
		return op;
	}
	
	private int getFunctionIndex(String symbol) {
		int index=0;
		for(int i=0;i<functionName.size();i++) {
			if(symbol.equals(functionName.get(i))) {
				return index+1;
			}
		}
		return -1;
	}

	private int getGlobalVarIndex(String symbol) {
		int index=0;
		for(index = 0; index<this.globalVariableNames.length;index++)
		{
			if(symbol.equals(this.globalVariableNames[index]))
				return index+1;
		}
		return 0;
	}

	private int getLocalVarIndex(String symbol) {
		int index=0;
		for(index = 0; index<this.functionEnvs.get(whichLocalEnv).length;index++)
		{
			if(symbol.equals(this.functionEnvs.get(whichLocalEnv)[index]))
				return index+1;
		}
		return 0;
	}
	
	private int getFloatIndex(String symbol) {
		int index=0;
		for(index = 0; index<this.floats.length;index++)
		{
			if(Double.parseDouble(symbol) == this.floats[index])
				return index+1;
		}
		return -1;
	}


	private int getStringIndex(String symbol) {
		int index=0;
		for(index = 0; index<this.strings.length;index++)
		{
			if(symbol.equals(this.strings[index]))
				return index+1;
		}
		return -1;
	}
	
	private int getMainPc()
	{
		return finfo[main_index].getPc();
	}

	public CVM loadCVM()
	{
		
		CVM cvm = new CVM(pool, bytecodes);
		cvm.setBeginPC(getMainPc());
		// allocate the space for the local variables of main function
		cvm.stackArea.alloca(finfo[main_index].getVariableCount());
		cvm.setBreakPoints(breakPointsPC);
		cvm.setFunctionEnvs(functionEnvs);
		cvm.setGlobalVariableNames(globalVariableNames);
		//cvm.testRun();
		return cvm;
	}
	

}

